2.3.2 feed.go
现在已经看过了 Run
函数,让我们继续看看search.go文件的第14行中的 RetrieveFeeds
函数调用背后的代码。这个函数读取data.json文件并返回数据源的切片。这些数据源会输出内容,随后使用各自的匹配器进行搜索。代码清单2-26给出的是feed.go文件的前8行代码。
代码清单2-26 feed.go:第01行到第08行
01 package search
02
03 import (
04 "encoding/json"
05 "os"
06 )
07
08 const dataFile = "data/data.json"
这个代码文件在 search
文件夹里,所以第01行声明了包的名字为 search
。第03行到第06行导入了标准库中的两个包。 json
包提供编解码JSON的功能, os
包提供访问操作系统的功能,如读文件。
读者可能注意到了,导入 json
包的时候需要指定 encoding
路径。不考虑这个路径的话,我们导入包的名字叫作 json
。不管标准库的路径是什么样的,并不会改变包名。我们在访问 json
包内的函数时,依旧是指定 json
这个名字。
在第08行,我们声明了一个叫作 dataFile
的常量,使用内容是磁盘上根据相对路径指定的数据文件名的字符串做初始化。因为Go编译器可以根据赋值运算符右边的值来推导类型,声明常量的时候不需要指定类型。此外,这个常量的名称使用小写字母开头,表示它只能在 search
包内的代码里直接访问,而不暴露到包外面。
接着我们来看看 data.json
数据文件的部分内容,如代码清单2-27所示。
代码清单2-27 data.json
[
{
"site" : "npr",
"link" : "http://www.npr.org/rss/rss.php?id=1001",
"type" : "rss"
},
{
"site" : "cnn",
"link" : "http://rss.cnn.com/rss/cnn_world.rss",
"type" : "rss"
},
{
"site" : "foxnews",
"link" : "http://feeds.foxnews.com/foxnews/world?format=xml",
"type" : "rss"
},
{
"site" : "nbcnews",
"link" : "http://feeds.nbcnews.com/feeds/topstories",
"type" : "rss"
}
]
为了保证数据的有效性,代码清单2-27只选用了4个数据源,实际数据文件包含的数据要比这4个多。数据文件包括一个JSON文档数组。数组的每一项都是一个JSON文档,包含获取数据的网站名、数据的链接以及我们期望获得的数据类型。
这些数据文档需要解码到一个结构组成的切片里,以便我们能在程序里使用这些数据。来看看用于解码数据文档的结构类型,如代码清单2-28所示。
代码清单2-28 feed.go:第10行到第15行
10 // Feed 包含我们需要处理的数据源的信息
11 type Feed struct {
12 Name string `json:"site"`
13 URI string `json:"link"`
14 Type string `json:"type"`
15 }
在第11行到第15行,我们声明了一个名叫 Feed
的结构类型。这个类型会对外暴露。这个类型里面声明了3个字段,每个字段的类型都是字符串,对应于数据文件中各个文档的不同字段。每个字段的声明最后 `` 引号里的部分被称作标记(tag)。这个标记里描述了JSON解码的元数据,用于创建
Feed` 类型值的切片。每个标记将结构类型里字段对应到JSON文档里指定名字的字段。
现在可以看看search.go代码文件的第14行中调用的 RetrieveFeeds
函数了。这个函数读取数据文件,并将每个JSON文档解码,存入一个 Feed
类型值的切片里,如代码清单2-29所示。
代码清单2-29 feed.go:第17行到第36行
17 // RetrieveFeeds读取并反序列化源数据文件
18 func RetrieveFeeds() ([]*Feed, error) {
19 // 打开文件
20 file, err := os.Open(dataFile)
21 if err != nil {
22 return nil, err
23 }
24
25 // 当函数返回时
26 // 关闭文件
27 defer file.Close()
28
29 // 将文件解码到一个切片里
30 // 这个切片的每一项是一个指向一个Feed类型值的指针
31 var feeds []*Feed
32 err = json.NewDecoder(file).Decode(&feeds)
33
34 // 这个函数不需要检查错误,调用者会做这件事
35 return feeds, err
36 }
让我们从第18行的函数声明开始。这个函数没有参数,会返回两个值。第一个返回值是一个切片,其中每一项指向一个 Feed
类型的值。第二个返回值是一个 error
类型的值,用来表示函数是否调用成功。在这个代码示例里,会经常看到返回 error
类型值来表示函数是否调用成功。这种用法在标准库里也很常见。
现在让我们看看第20行到第23行。在这几行里,我们使用 os
包打开了数据文件。我们使用相对路径调用 Open
方法,并得到两个返回值。第一个返回值是一个指针,指向 File
类型的值,第二个返回值是 error
类型的值,检查 Open
调用是否成功。紧接着第21行就检查了返回的 error
类型错误值,如果打开文件真的有问题,就把这个错误值返回给调用者。
如果成功打开了文件,会进入到第27行。这里使用了关键字 defer
,如代码清单2-30所示。
代码清单2-30 feed.go:第25行到第27行
25 // 当函数返回时
26 // 关闭文件
27 defer file.Close()
关键字 defer
会安排随后的函数调用在函数返回时才执行。在使用完文件后,需要主动关闭文件。使用关键字 defer
来安排调用 Close
方法,可以保证这个函数一定会被调用。哪怕函数意外崩溃终止,也能保证关键字 defer
安排调用的函数会被执行。关键字 defer
可以缩短打开文件和关闭文件之间间隔的代码行数,有助提高代码可读性,减少错误。
现在可以看看这个函数的最后几行,如代码清单2-31所示。先来看一下第31行到第35行的代码。
代码清单2-31 feed.go:第29行到第36行
29 // 将文件解码到一个切片里
30 // 这个切片的每一项是一个指向一个Feed类型值的指针
31 var feeds []*Feed
32 err = json.NewDecoder(file).Decode(&feeds)
33
34 // 这个函数不需要检查错误,调用者会做这件事
35 return feeds, err
36 }
在第31行我们声明了一个名字叫 feeds
,值为 nil
的切片,这个切片包含一组指向 Feed
类型值的指针。之后在第32行我们调用 json
包的 NewDecoder
函数,然后在其返回值上调用 Decode
方法。我们使用之前调用 Open
返回的文件句柄调用 NewDecoder
函数,并得到一个指向 Decoder
类型的值的指针。之后再调用这个指针的 Decode
方法,传入切片的地址。之后 Decode
方法会解码数据文件,并将解码后的值以 Feed
类型值的形式存入切片里。
根据 Decode
方法的声明,该方法可以接受任何类型的值,如代码清单2-32所示。
代码清单2-32 使用空interface
func (dec *Decoder) Decode(v interface{}) error
Decode
方法接受一个类型为 interface{}
的值作为参数。这个类型在Go语言里很特殊,一般会配合 reflect
包里提供的反射功能一起使用。
最后,第35行给函数的调用者返回了切片和错误值。在这个例子里,不需要对 Decode
调用之后的错误做检查。函数执行结束,这个函数的调用者可以检查这个错误值,并决定后续如何处理。
现在让我们看看搜索的代码是如何支持不同类型的数据源的。让我们去看看匹配器的代码。